﻿//NorManager - OpenTTD Dedicated server manager
//Copyright (C) 2014 Tomáš Bosek

//This program is free software: you can redistribute it and/or modify
//it under the terms of the GNU General Public License as published by
//the Free Software Foundation, either version 3 of the License.

//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.

//You should have received a copy of the GNU General Public License
//along with this program.  If not, see <http://www.gnu.org/licenses/>.
using System;
using System.IO;
using System.Threading;
using System.Text;
using System.Diagnostics;
using System.Collections.Generic;

namespace NorManager
{
    public delegate void SessionStateHandler();
    /// <summary>
    /// Struct for storing Session configuration.
    /// </summary>
    public struct SessionConfig
    {
        /// <summary>
        /// Openttd executable file path.
        /// </summary>
        public string ExecPath { get { if (execPath == null) { execPath = String.Empty; } return execPath; } }
        private string execPath;
        
        /// <summary>
        /// Openttd config file path.
        /// </summary>
        public string ConfigPath { get { if (configPath == null) { configPath = String.Empty; } return configPath; } }
        private string configPath;

        /// <summary>
        /// Autoloading last savegames
        /// </summary>
        public bool Autoload { get { return autoload; } }
        private bool autoload;
        /// <summary>
        /// Openttd executable and/or config in different directory. 
        /// Otherwise use constructor without parameters or String.Empty
        /// </summary>
        /// <param name="execPath">Openttd executable path. Leave empty for the same directory as this program.</param>
        /// <param name="configPath">Openttd config file path. Leave empty for OpenTTD default config path.</param>
        public SessionConfig(string execPath, string configPath, bool autoload)
        {
            this.execPath = execPath;
            this.configPath = configPath;
            this.autoload = autoload;
        }
    }
    /// <summary>
    /// Session states.
    /// </summary>
    public enum SessionState
    {
        Closed = 0,
        UnableToStart,
        Running,
        Crashed
    }
    /// <summary>
    /// Session class handles server process and reading+writing it's stream.
    /// </summary>
    public class Session
    {
        public static Session Instance { get { if (instance == null) { instance = new Session(); } return instance; } }
        private static Session instance = null;

        /// <summary>
        /// Config used last time server started.
        /// </summary>
        public SessionConfig Config { get { return config; } }
        private SessionConfig config = new SessionConfig();
        /// <summary>
        /// Game loaded last time.
        /// </summary>
        public string LastSave { get { return LastSave; } }
        private string lastSave;

        public SessionState State { get { return state; } }
        private SessionState state = SessionState.Closed;

        internal Process SessionProc { get { return sessionProc; } }
        private Process sessionProc;

        //Python script classes. One for "runOnce" scripts, second for scripts that are executed each StartSession
        private PythonScripts runOnceScripts = new PythonScripts();
        private PythonScripts runAlwaysScripts = new PythonScripts();

        private CrashObserver crashObserver;

        public SessionWriter Writer { get { return writer; } }
        private SessionWriter writer;
        public SessionReader Reader { get { return reader; } }
        private SessionReader reader;
        /// <summary>
        /// List all active of filters.
        /// </summary>
        public List<Filter> FilterList { get { return filterList; } }
        private List<Filter> filterList = new List<Filter>();

        private SessionStateHandler stateChanged;
        public SessionStateHandler Running;
        public SessionStateHandler UnableToStart;
        public SessionStateHandler Crashed;
        public SessionStateHandler Closed;

        private Session()
        {
            SessionStateHandler echoHandler = new SessionStateHandler(delegate
            {
                Logger.Instance.Log(String.Format("Session state changed({0}).", state));
            });
            Running += echoHandler;
            UnableToStart += echoHandler;
            Crashed += echoHandler;
            Closed += echoHandler;

            #region Reaction on state changing.
            stateChanged += new SessionStateHandler(delegate
            {
                switch (state)
                {
                    case SessionState.Closed:
                        Closed();
                        break;
                    case SessionState.Crashed:
                        CloseSession();
                        Crashed();
                        StartSession();
                        break;
                    case SessionState.Running:
                        Running();
                        break;
                    case SessionState.UnableToStart:
                        UnableToStart();
                        break;
                }
            });
            #endregion

            crashObserver = new CrashObserver();
            crashObserver.crashed += new CrashObserverHandler(delegate 
                {
                    ChangeState(SessionState.Crashed);
                });

            runOnceScripts.LoadScripts("o_");
            runOnceScripts.ExecuteScripts();
        }

        /// <summary>
        /// Used for changing state and calling the events.
        /// </summary>
        /// <param name="state">New session state.</param>
        private void ChangeState(SessionState state)
        {
            this.state = state;
            stateChanged();
        }

        /// <summary>
        /// Used for changing SessionConfig.
        /// </summary>
        /// <param name="config">New configuration.</param>
        public void ChangeSessionConfig(SessionConfig config)
        {
            if (state != SessionState.Running)
                this.config = config;
            else
                Logger.Instance.Log(String.Format("Cannot change SessionConfig in current state({0}).", state));
        }

        /// <summary>
        /// Starts new session with last config.
        /// </summary>
        public void StartSession()
        {
            StartSession(config);
        }

        /// <summary>
        /// Starts new session with new config.
        /// </summary>
        /// <param name="config">Session config.</param>
        public void StartSession(SessionConfig newconfig)
        {
            if (state == SessionState.Running || state == SessionState.UnableToStart)
            {
                Logger.Instance.Log(String.Format("It is not possible to start a new session in current state({0}).", state));
                return;
            }

            config = newconfig;

            if (CheckFiles())
            {
                if(Environment.OSVersion.ToString().Contains("Windows"))
                    ModifyHeaders(config.ExecPath);
            }
            else
            {
                ChangeState(SessionState.UnableToStart);
                return;
            }

            sessionProc = new Process();

            sessionProc.StartInfo.FileName = config.ExecPath;
            sessionProc.StartInfo.Arguments = "-D"+(config.ConfigPath!=String.Empty?" -c "+config.ConfigPath:String.Empty)+(lastSave!=String.Empty?" -g "+lastSave:String.Empty);
            sessionProc.StartInfo.UseShellExecute = false;
            sessionProc.StartInfo.RedirectStandardInput = true;
            sessionProc.StartInfo.RedirectStandardOutput = true;
            sessionProc.StartInfo.RedirectStandardError = true;
            //Set autoload to avoid creating new game after crash
            config = new SessionConfig(config.ExecPath, config.ConfigPath, true);

            //Clear filterList and handle "server could not be started" as UnableToStart
            filterList.Clear();
            Filter couldNotStarted = new Filter("server could not be started", delegate
            {
                ChangeState(SessionState.UnableToStart);
                CloseSession();
            });
            filterList.Add(couldNotStarted);

            //Start all services
            runAlwaysScripts.LoadScripts("a_");
            sessionProc.Start();
            crashObserver.Start();
            writer = new SessionWriter();
            reader = new SessionReader();

            ChangeState(SessionState.Running);

            //And now execute Python scripts
            runAlwaysScripts.ExecuteScripts();
        }

        /// <summary>
        /// Closes actually running session.
        /// </summary>
        public void CloseSession()
        {
            if (state == SessionState.Closed)
                return;

            crashObserver.Stop();
            writer.CloseWriter();
            reader.CloseReader();
            if (!sessionProc.HasExited)
                sessionProc.Kill();

            if (state != SessionState.UnableToStart && state != SessionState.Crashed)
                ChangeState(SessionState.Closed);
        }
        /// <summary>
        /// Reloads session. Use when you want to restart server with save before end and load on start.
        /// </summary>
        public void ReloadSession()
        {
            if (sessionProc != null && !sessionProc.HasExited && state == SessionState.Running)
            {
                long ticks = DateTime.Now.Ticks;
                string savename = "normanager"+ticks;
                Filter reloadFilter = null;
                reloadFilter = new Filter(savename+".sav", delegate
                {
                    Session.Instance.CloseSession();
                    Session.Instance.StartSession();
                });

                filterList.Add(reloadFilter);
                writer.Write(String.Format("save \"{0}\"", savename));      //Send save command
            }
        }
        /// <summary>
        /// Checks executable and config file paths.
        /// </summary>
        /// <returns>True if exec file exists. False if not.</returns>
        private bool CheckFiles()
        {
            string execpath, configpath;

            execpath = config.ExecPath;
            configpath = config.ConfigPath;

            //Executable == Empty string => try to find executable in this program's directory
            if (execpath == String.Empty)
            {
                execpath = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location) + "/openttd" + (Environment.OSVersion.ToString().Contains("Windows") ? ".exe" : String.Empty);
                config = new SessionConfig(execpath, config.ConfigPath, config.Autoload);
            }

            FileInfo info = new FileInfo(execpath);
            //Cannot find executable. Return false!
            if (!info.Exists)
            {
                Logger.Instance.Log("Executable file not found in destination directory(nor in the current directory). Unable to start session.");
                return false;
            }

            //It's not necessary to specify config path. Game can create new one.
            if (!File.Exists(configpath))
                Logger.Instance.Log("Config path not found. Config parameter will not be used.");

            if (config.Autoload)
            {
                SaveManager savemanager = new SaveManager((File.Exists(config.ConfigPath) ? config.ConfigPath : config.ExecPath));
                lastSave = savemanager.FindLastSave();
                if(lastSave != String.Empty)
                    Logger.Instance.Log(String.Format("Loading game: {0}", lastSave));
            }
            return true;
        }

        /// <summary>
        /// Modifies openttd.exe headers to make it console app.
        /// </summary>
        private void ModifyHeaders(string path)
        {
            try
            {
                FileStream exec = new FileStream(path, FileMode.Open, FileAccess.ReadWrite);
                byte[] buffer = new byte[exec.Length];
                exec.Read(buffer, 0, buffer.Length);

                int offset = (((buffer[0x3f] << 8 | buffer[0x3e]) << 8 | buffer[0x3d]) << 8 | buffer[0x3c]) + 92;

                if (buffer[offset] == 2)
                {
                    Logger.Instance.Log("Modifying headers.");
                    buffer[offset] = 3;
                    exec.Seek(0, SeekOrigin.Begin);
                    exec.Write(buffer, 0, buffer.Length);
                }
                exec.Dispose();
            }
            catch (Exception)
            {
                Logger.Instance.Log("Cannot reach executable header data.");
            }
        }
    }
}
